﻿using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

namespace MvvmFx.Common.ViewModels.Commands
{
    /// <summary>
    /// The commands class provides the support to attach commands 
    /// in Silverlight to all controls that have a base class of <see cref="ButtonBase"/>.
    /// </summary>
    /// <example>
    /// <code>
    ///  <Button 
    ///     Commands:Commands.Command="{Binding MyCommand}"
    ///     Commands:Commands.CommandParameter="{Binding MyParameter}" />
    /// </code>
    /// </example>
    public static class Commands
    {
        #region [ Commmand Button Behavior Attached Property ]

        /// <summary>
        /// Atached dependency propety to set the command behavior.
        /// </summary>
        private static readonly DependencyProperty CommandButtonBehaviorProperty =
            DependencyProperty.RegisterAttached("CommandButtonBehavior", typeof(CommandButtonBehavior),
                                                typeof(Commands), null);

        #endregion

        #region [ Command Attached Property]

        /// <summary>
        /// Atached dependency propety to enable a command to be attached to a control .
        /// </summary>
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(Commands),
                                                new PropertyMetadata(CommandPropertyChanged));

        /// <summary>
        /// Gets the command attched to the button.
        /// </summary>
        public static ICommand GetCommand(ButtonBase buttonBase)
        {
            return (ICommand)buttonBase.GetValue(CommandProperty);
        }

        /// <summary>
        /// Set the command for the associated button control <see cref="ButtonBase"/>.
        /// </summary>
        /// <param name="buttonBase">The button control.</param>
        /// <param name="value">The command to attach to the button.</param>
        public static void SetCommand(ButtonBase buttonBase, ICommand value)
        {
            buttonBase.SetValue(CommandProperty, value);
        }

        /// <summary>
        /// Occurs when the <see cref="DependencyObject"/> changes. Sets the command behavior for the specified 
        /// button control.
        /// </summary>
        /// <param name="obj">The <see cref="DependencyPropertyChangedEventArgs"/> object associated to the attached dependency property.</param>
        /// <param name="e">The <see cref="Command"/> event fired.</param>
        private static void CommandPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            ButtonBase buttonBase = obj as ButtonBase;
            if (buttonBase != null)
            {
                if (e.OldValue != null)
                {
                    DetachCommand(buttonBase);
                }
                if (e.NewValue != null)
                {
                    AttachCommand(buttonBase, (ICommand)e.NewValue);
                }
            }
        }

        #endregion

        #region [ Command Parameter Attached Property ]

        /// <summary>
        /// Attached property to define the parameters to be passed to the command when executed.
        /// </summary>
        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.RegisterAttached("CommandParameter", typeof(object), typeof(Commands),
                                                null);
        /// <summary>
        /// Get the command parameter for the button control <see cref="ButtonBase"/>.
        /// </summary>
        /// <param name="buttonBase">The button control <see cref="ButtonBase"/> to set the command parameters.</param>
        public static object GetCommandParameter(ButtonBase buttonBase)
        {
            return buttonBase.GetValue(CommandParameterProperty);
        }

        /// <summary>
        /// Sets the command parameters for the button control <see cref="ButtonBase"/>.
        /// </summary>
        /// <param name="buttonBase">The button control <see cref="ButtonBase"/>.</param>
        /// <param name="value">The command parameter for button control <see cref="ButtonBase"/>.</param>
        public static void SetCommandParameter(ButtonBase buttonBase, object value)
        {
            buttonBase.SetValue(CommandParameterProperty, value);
        }

        #endregion

        #region [ Private Methods ]

        /// <summary>
        /// Attaches the command for the specified button. 
        /// </summary>
        /// <param name="buttonBase">The button control <see cref="ButtonBase"/> to attach the command to.</param>
        /// <param name="command">The command <see cref="ICommand"/> to be attached to the button.</param>
        private static void AttachCommand(ButtonBase buttonBase, ICommand command)
        {
            CommandButtonBehavior behavior = new CommandButtonBehavior(buttonBase, command);
            behavior.Attach();
            buttonBase.SetValue(CommandButtonBehaviorProperty, behavior);
        }

        /// <summary>
        /// Detaches the command from the button control <see cref="ButtonBase"/>.
        /// </summary>
        /// <param name="buttonBase">The button control <see cref="ButtonBase"/> to remove the command from.</param>
        private static void DetachCommand(ButtonBase buttonBase)
        {
            CommandButtonBehavior behavior = (CommandButtonBehavior)buttonBase.GetValue(CommandButtonBehaviorProperty);
            if (behavior != null)
            {
                behavior.Detach();
            }
            buttonBase.ClearValue(CommandButtonBehaviorProperty);
        }

        #endregion
        
        /// <summary>
        /// The CommandButtonBehavior class that provides the 
        /// behavior support for all button controls <see cref="ButtonBase"/>.
        /// </summary>
        private class CommandButtonBehavior
        {
            #region [ Private Members ]
            
            private readonly WeakReference elementReference;
            private readonly ICommand command;
            
            #endregion

            /// <summary>
            /// Initializes a new instance of the <see cref="Infusion.Silverlight.Common.Commands.CommandButtonBehavior"/> class.
            /// </summary>
            /// <param name="buttonBase">The button control <see cref="ButtonBase"/> to attach the behavior to.</param>
            /// <param name="command">The command to attach to the command.</param>
            public CommandButtonBehavior(ButtonBase buttonBase, ICommand command)
            {
                this.elementReference = new WeakReference(buttonBase);
                this.command = command;
            }

            #region [ Public Methods ]

            /// <summary>
            /// Attach the button command to the button control <see cref="ButtonBase"/>.
            /// </summary>
            public void Attach()
            {
                ButtonBase buttonBase = GetElement();
                if (buttonBase != null)
                {
                    buttonBase.Click += Button_Clicked;
                    command.CanExecuteChanged += Command_CanExecuteChanged;
                    SetIsEnabled(buttonBase);
                }
            }

            /// <summary>
            /// Detach the command for the button control <see cref="ButtonBase"/>
            /// </summary>
            public void Detach()
            {
                command.CanExecuteChanged -= Command_CanExecuteChanged;
                ButtonBase buttonBase = GetElement();
                if (buttonBase != null)
                {
                    buttonBase.Click -= Button_Clicked;
                }
            }

            #endregion

            #region [ Private Methods ]

            /// <summary>
            /// Set the enabled state of the button control <see cref="ButtonBase"/>.
            /// </summary>
            /// <param name="buttonBase">The button control to set the enabled state.</param>
            private void SetIsEnabled(ButtonBase buttonBase)
            {
                buttonBase.IsEnabled = command.CanExecute(buttonBase.GetValue(CommandParameterProperty));
            }

            /// <summary>
            /// Get the button control <see cref="ButtonBase"/>.
            /// </summary>
            /// <returns>The button control <see cref="ButtonBase"/>.</returns>
            private ButtonBase GetElement()
            {
                return elementReference.Target as ButtonBase;
            }

            #endregion 

            #region [ Event Handlers ]

            /// <summary>
            /// Occurs when the <see cref="ICommand.CanExecuteChanged"/> changes. Set the enabled state for the command control.
            /// </summary>
            /// <param name="sender">The <see cref="object"/> object that fired the event.</param>
            /// <param name="e">The <see cref="EventArgs"/> event fired.</param>
            void Command_CanExecuteChanged(object sender, EventArgs e)
            {
                ButtonBase element = GetElement();
                if (element != null)
                {
                    SetIsEnabled(element);
                }
                else
                {
                    Detach();
                }
            }

            /// <summary>
            /// Occurs when the <see cref="ButtonBase.Click"/> changes. Execute the command 
            /// when the button control is clicked.
            /// </summary>
            /// <param name="sender">The <see cref="object"/> object that fired the event.</param>
            /// <param name="e">The <see cref="EventArgs"/> event fired.</param>
            private static void Button_Clicked(object sender, EventArgs e)
            {
                DependencyObject element = (DependencyObject)sender;
                ICommand command = (ICommand)element.GetValue(CommandProperty);
                object commandParameter = element.GetValue(CommandParameterProperty);
                command.Execute(commandParameter);
            }

            #endregion
        }
    }
}